My Calendar

2018年6月23日 星期六

Node.js 開發Slack Bot - 3

      Slack API 可以設定當某些事件觸發的時候才執行該方法,如之前使用到的"connected"事件,當Bot成功連接Slack才會觸發。相關文件可以到http://slackapi.github.io/node-slack-sdk/rtm_api查看。

如果希望在Bot登入但連接到頻道之前可以用AUTHENTICATED。
slack.on("authenticated",(connectData)=>{
  console.log(`Logged in as ${connectData.self.name} of team ${connectData.team.name}, but not yet connected to a channel`);
});

當等你將會看到以下的訊息。
Logged in as hellobot of team Deikhoong, but not yet connected to a channel

Bot要接收使用者的訊息可以使用MESSAGE EVENT. 只要每次將訊息發送到Bot所在的頻道或向Bot發送Direct Message時,這個Event都會被觸發。當我們發送"Hi Bot"或"Hello Bot",Bot會回覆給發送者的頻道。

slack.on('message', (message) => {

  (async() =>{
      let user= {"user":message.user};
      let userInfo = await getUserInfo(user);
      console.log(userInfo.user.is_bot);
      if (userInfo && userInfo.user.is_bot) {
          return;
      }
      
      let channel = message.channel;
      if (message.text) {
          console.log(`Incoming message: ${message.text}`);
          let msg = message.text.toLowerCase();
          if (/(hello|hi) bot/g.test(msg)) {
            slack.sendMessage(`Hello to you too, ${userInfo.user.name}!`,channel);
          }
      }
  })();
     
});


結果如下










傳送Direct Message
Direct Message (DM) 是一個僅在兩個用戶之間運行的頻道。根據設計,它不能有多於或少於兩個用戶,並且用於私人通信。 發送DM與向頻道發送消息非常相似。

在index.js新增getDM() function和片段程式
slack.on('message', (message) => {

  (async() =>{
      let user= {"user":message.user};
      let userInfo = await getUserInfo(user);
      let dmInfo = await getDM(user);
      if (userInfo && userInfo.user.is_bot) {
          return;
      }
      
      let channel = message.channel;
      if (message.text) {
          console.log(`Incoming message: ${message.text}`);
          let msg = message.text.toLowerCase();

          if(/uptime/g.test(msg))
          {
            let uptime = process.uptime();
            let minutes = parseInt(uptime / 60, 10),
            hours = parseInt(minutes / 60, 10),
            seconds = parseInt(uptime % 60, 10);
            slack.sendMessage(`I have been running for: ${hours} hours,${minutes} minutes and ${seconds} seconds.`, dmInfo.channel.id);
          }else if (/(hello|hi) bot/g.test(msg)) {
            slack.sendMessage(`Hello to you too, ${userInfo.user.name}!`,channel);
          }
      }
  })();
     
});

function getDM(user) 
{
   return web.im.open(user).then((resp)=>{
      return resp;
   }).catch((error) => {console.log(error);});   
}

在這個例子中,我們的Bot會將當前正常運行時間透過DM發送給任何發送"uptime"關鍵字的用戶。









































限制訪問
偶爾,我們會希望Bot某些指令只允許Admin才能使用。我們使用之前的例子uptime 指令只能讓Admin才能使用,其他user使用將會回傳warning 訊息。只要透過userInfo判斷該user是否admin即可。
if(/uptime/g.test(msg)){
     if (!userInfo.user.is_admin) {
          slack.sendMessage(`Sorry ${userInfo.user.name}, but that functionality is only for admins.`, dmInfo.channel.id);
          return;
     }
            
     let uptime = process.uptime();
     let minutes = parseInt(uptime / 60, 10),
     hours = parseInt(minutes / 60, 10),
     seconds = parseInt(uptime % 60, 10);
     slack.sendMessage(`I have been running for: ${hours} hours,${minutes} minutes and ${seconds} seconds.`, dmInfo.channel.id);  
}




2018年6月21日 星期四

Node.js 開發Slack Bot - 2

       Node.js開發Slack Bot - 1已經有一個連接到Slack的Chat Bot, 現在實作當Bot上線後會發送"Hello"給有邀請Bot的全部頻道。

在原本的index.js新增getAllChannels() function
function getAllChannels()
{
   return web.channels.list().then((resp)=>{
      return resp;
   }).catch((error) => {console.log(error);});
}

在connected event呼叫getAllChannels和判斷Bot在哪一個頻道。

slack.on('connected', () => {

  let user= {"user":slack.activeUserId};

  (async() =>{
             
    let userInfo = await getUserInfo(user);
    let teamInfo = await getTeamInfo();
    let allChannels = await getAllChannels();
    let channels = [];
    for (let id in allChannels.channels) {
          let channel = allChannels.channels[id];
          if(channel.is_member)
            channels.push(channel);
    }  

    let channelNames = channels.map((channel) => {
                          return channel.name;
                        }).join(', ');         

    console.log(`Connected to ${teamInfo.team.name} as ${userInfo.user.name}`);
    console.log(`Currently in: ${channelNames}`);
  })();

});

切換Terminal會看到以下類似的輸出,這時候已經知道Bot在哪一個頻道,開始發送"Hello"給這些頻道的所有人。

Connected to Deikhoong as helloBot
Currently in : bot-test

獲取頻道中的全部人
我們已經有了channels這個物件,那需要得到頻道中的人就變得非常簡單。
channels.forEach((channel) => {
     console.log('Members of this channel: ', channel.members);
});

結果如下,
Connected to Deikhoong as hellobot
Currently in: bot-test
Members of this channel:  [ 'U1GUZL79C', 'UBGN5T9K2' ]

發現channels.members回傳的不是member的物件而是member id, 這會讓我們很難辨識.我們在改寫這段程式碼使用直接的getUserInfo()獲得 user的名稱。
let members=[];
channels.forEach(async (channel) => {
     for (let id in channel.members) {
        let memberId = channel.members[id];      
        user= {"user":memberId};
        let member= await getUserInfo(user);
        members.push(member);
     }
     let memberNames = members.map((member) => {
              return member.user.name;
      }).join(', ');
     console.log('Members of this channel: ', memberNames);
});

已經可以看到user的名稱
Connected to Deikhoong as hellobot
Currently in: bot-test
Members of this channel: tan.deik.hoong, hellobot

發送訊息到頻道
透過RTMClient.sendMessage發送訊息給頻道。

slack.on('connected', () => {

  let user= {"user":slack.activeUserId};

  (async() =>{
             
    let userInfo = await getUserInfo(user);
    let teamInfo = await getTeamInfo();
    let allChannels = await getAllChannels();
    let channels = [];
    for (let id in allChannels.channels) {
          let channel = allChannels.channels[id];
          if(channel.is_member)
            channels.push(channel);
    }  

    let channelNames = channels.map((channel) => {
                          return channel.name;
                        }).join(', '); 


    console.log(`Connected to ${teamInfo.team.name} as ${userInfo.user.name}`);
    console.log(`Currently in: ${channelNames}`);

    let members=[];
    channels.forEach(async (channel) => {
         for (let id in channel.members) {
            let memberId = channel.members[id];      
            user= {"user":memberId};
            let member= await getUserInfo(user);
            members.push(member);
         }
         let memberNames = members.map((member) => {
              return member.user.name;
          }).join(', ');
        console.log('Members of this channel: ', memberNames);
        //send message to channel
        slack.sendMessage(`Hello ${memberNames}!`, channel.id);
    });

  })();

});

Bot每次上線的時候都會發送訊息給全部頻道的人。

2018年6月20日 星期三

Node.js 開發Slack Bot -1

在開發Slack Bot之前需要準備環境和套件。

1) 安裝Node.js
2) 安裝nodemon 套件
  • npm install -g nodemon
3) 創建新Project和安裝slack client 套件
  • mkdir botTest && cd botTest
  • npm init
  • npm install @slack/client --save  (目前版本是4.0++)
4) 創建Slack API Token

     開啟瀏覽器輸入以下網址(my.slack.com 修改成自己的網址)                     
     https://my.slack.com/apps/build/customintegration.

按照以下步驟:

1) 選擇 Custom Integrations


2)選擇 Add Configuration 和設定Bots的名稱

3) 獲取API Token 
環境準備好,那就開始我們第一支程式。

新增檔案index.js, 程式碼如下:
'use strict';

// Import the Real Time Messaging (RTM) client
const RtmClient = require('@slack/client').RTMClient;

// Import the Web Client
const WebClient = require('@slack/client').WebClient;

//Slack API Token
const token = "";

const web = new WebClient(token);

let slack = new RtmClient(token);

// When the bot connects success
slack.on('connected', () => {

  let user= {"user":slack.activeUserId};

   (async() =>{
             
    let userInfo = await getUserInfo(user);
    let teamInfo = await getTeamInfo();
    console.log(`Connected to ${teamInfo.team.name} as ${userInfo.user.name}`);
  })();

});

// Start the login process
slack.start();


function getUserInfo(user)
{
    return web.users.info(user).then((resp) => {
        return resp;
    }).catch((error) => {console.log(error);});;
}

function getTeamInfo()
{
    return  web.team.info().then((resp) => {
         return resp;
    }).catch((error) => {console.log(error);});
}

當你在Terminal 看到類似的內容, 代表你的Chat Bot已經成功連結到Slack.

Connected to "Deikhoong" as "hellobot"





















加入頻道
Bot 不能透過編程的方式加入頻道,這是防止Bot在沒有被邀請的情況下加入私人頻道。當Bot加入了頻道,頻道中的全部活動都會被Bot 監控。Bot相關可以執行和不可執行的操作在 https://api.slack.com/bot-users 有完整的文件說明。

要將Bot加入頻道可以通過Slack Client中的invite指令. 輸入後將會受到Bot加入頻道的確認訊息。要將Bot從頻道移除一樣通過Slack Client中的remove指令即可。











2016年8月2日 星期二

Uri.fromFile中文亂碼

Uri.fromFile 中文亂碼

最近在開發 APP 的時候發現有某部分拍照後有確認和取消按鈕的手機會讓整個 APP 崩潰。花了一些時間最後找到兇手原來就是 Uri.fromFile已知 ASUS_T00P 會有這個問題。

URl使用中文去訪問是沒有問題的,因为android在內部進行了轉碼。 但是從URL獲取文件名的時候,因为android已經轉過碼,所以得到的就是亂碼了。如果一定需要中文路徑或是中文檔案名稱,可以用到android提供的方法Uri.decode()獲取正確的中文路徑或是直接使用Uri.parse()


 String imageRef= "/mnt/sdcard/測試.jpg";;
 File f = new File(imageRef);
 Uri uri = Uri.fromFile(f); //會造成檔案亂碼
 Uri uri =  Uri.parse("file://"+imageRef.getPath()); // 使用Uri.parse()
 Uri uri =  Uri.parse(Uri.decode(path)); // 使用Uri.decode() 轉碼

2015年2月10日 星期二

Servlet 檔案上傳

          要實現檔案上傳之前, 首先要了解 html中enctype這個屬性, enctype 屬性是在表單要發送數據給server之前決定用哪一種編碼。

enctype 有三種屬性:
  • application/x-www-form-urlencoded:默認編碼方式(只是處理表單裡value屬性值)
  • multipart/form-data:會以binary的編碼方式處理數據
  • text/plain :空格轉為"+",但不对特殊字元編碼
既然需要實現檔案上傳, 那我們的enctype當然要用multipart/form-data這個屬性。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>文件上傳</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <div>
            <form action="upload" method="POST" enctype="multipart/form-data">
                <table>
                    <tr>
                        <td><label for="file1">文件:</label></td>
                        <td><input type="file" id="file" name="file"></td>
                    </tr>
                    <tr>
                        <td colspan="2"><input type="submit" value="上传" name="upload"></td>
                    </tr>
                </table>
            </form>
        </div>
    </body>
</html>

Servlet 2.x 的版本有兩種方法可以獲取上傳的檔案。

1.使用getInputStream()處理binary資料
package servlet.upload.demo;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

 class Position {

        int begin;
        int end;

  public Position(int begin, int end) {
   this.begin = begin;
   this.end = end;
  }
 }

 private static final long serialVersionUID = -4673819464292623830L;

 public UploadServlet() {
  super();
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

  byte[] content = bodyContent(request); // 讀取HTTP Body的內容
  String contentAsTxt = new String(content, "ISO-8859-1"); // 將HTTP Body內容用String表示

  String filename = getFileName(contentAsTxt); // 取得檔案名稱
  Position p = getFilePosition(contentAsTxt, request.getContentType()); // 取得檔案開始和結束位置

  writeTo(filename, content, p);
 }

 private byte[] bodyContent(HttpServletRequest request) throws IOException {

  ByteArrayOutputStream out = new ByteArrayOutputStream();
  InputStream in = request.getInputStream();

  try {
   byte[] buffer = new byte[1024];
   int length = -1;
   while ((length = in.read(buffer)) != -1) {
    out.write(buffer, 0, length);
   }
  } catch (Exception e) {
   e.printStackTrace();
  }

  return out.toByteArray();
 }

 private String getFileName(String requestBody) {
  String fileName = requestBody.substring(requestBody.indexOf("filename=\"") + 10);
  fileName = fileName.substring(0, fileName.indexOf("\n"));
  fileName = fileName.substring(fileName.indexOf("\n") + 1, fileName.indexOf("\""));

  return fileName;
 }

 private Position getFilePosition(String content, String contentType) throws IOException {
  String boundaryText = contentType.substring(contentType.lastIndexOf("=") + 1, contentType.length());
  // 取得實際上傳檔案的起始与结束位置
  int pos = content.indexOf("filename=\"");
  pos = content.indexOf("\n", pos) + 1;
  pos = content.indexOf("\n", pos) + 1;
  pos = content.indexOf("\n", pos) + 1;
  int boundaryLoc = content.indexOf(boundaryText, pos) - 4;
  int begin = ((content.substring(0, pos)).getBytes("ISO-8859-1")).length;
  int end = ((content.substring(0, boundaryLoc)).getBytes("ISO-8859-1")).length;

  return new Position(begin, end);
 }

 private void writeTo(String fileName, byte[] body, Position p) throws IOException {
  FileOutputStream fileOutputStream = new FileOutputStream("d:/" + fileName);
  fileOutputStream.write(body, p.begin, (p.end - p.begin));
  fileOutputStream.flush();
  fileOutputStream.close();
 }

}

實務上表格通常不會只有一個上傳檔案,會同時包含文字輸入,按鈕選項,下拉式選單等等。這時候如果使用request.getParameter()讀取資料,你會發現都拿到null!!!。因為表格的enctype 屬性已經設定成multipart/form-data,資料是以binary傳輸,Servlet 2.x版本的request.getParameter() 預設是不會處理binary的資料,所以得到的值都是null.

如果採用第一種方法那就需要將全部資料從binary截取,那實在太麻煩了。那我們用第三方套件幫組我們完成這件事。

2. 使用第三方套件 Commons File Upload

下載 commons-fileupload.jarcommons-io.jar 然後引入到專案中.
package servlet.upload.demo;

import java.io.File;
import java.util.List;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@WebServlet("/upload")
public class UploadServlet2 extends HttpServlet {

 private static final long serialVersionUID = 2372213113534302476L;

 public UploadServlet2() {
  super();
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) {
  try {
   //檢查請求是否用multipart/form-data處理
   if (ServletFileUpload.isMultipartContent(request)) {
    DiskFileItemFactory factory = new DiskFileItemFactory();
    ServletFileUpload upload = new ServletFileUpload(factory);
    //解析請求
    List formItems = upload.parseRequest(request);
    if (formItems != null && formItems.size() > 0) {
     for (FileItem item : formItems) {
      //檢查內容是檔案還是文字
      if (!item.isFormField() && item.getName() != null && !item.getName().equals("")) {
       String fileName = new File(item.getName()).getName();
       String filePath = "D:" + File.separator + fileName;
       File storeFile = new File(filePath);
       item.write(storeFile);
      } else {
       System.out.println("Key="+item.getFieldName());
       System.out.println("Value="+item.getString());
      }
     }
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
  }

 }

}


3.使用Servlet 3.0 @MultipartConfig
Servlet 3.0 開始HttpServletRequest提供上傳檔案的支持。
package servlet.upload.demo;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@MultipartConfig(location = "d:/")
@WebServlet("/upload")
public class UploadServlet3 extends HttpServlet {

 public UploadServlet3() {
  super();
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  request.setCharacterEncoding("UTF-8"); // 處理中文檔名
  Part part = request.getPart("file");
  part.write(part.getSubmittedFileName());
 }
}


2015年2月6日 星期五

使用RequestDispatcher 調派需求

在網頁應用程式中,經常需要多個Servlet完成需求,像是將一個Servlet的請求包含進來,或轉發給其他的Servlet處理。這時候可以使用 HttpServletRequest  getRequestDispatcher() 取得 RequestDispatcher,在這接口中定義了兩個方法:include() 和 forward()

RequestDispatcher disp = request.getRequestDispatcher("one.view");

使用include()
RequestDispatcher 的 include(),可以將另外一個Servlet的執行流程包含在目前Servlet的執行流程中。如下:

One.java
@WebServlet("/one.view")
public class One extends HttpServlet {
 private static final long serialVersionUID = 1L;

    public One() {
      super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  
        PrintWriter out = response.getWriter();
        out.println("First view Start");
        RequestDispatcher disp = request.getRequestDispatcher("two.view");
        disp.include(request, response);
        out.println("First view End");
    }

}

Two.java
@WebServlet("/two.view")
public class Two extends HttpServlet {
    
    private static final long serialVersionUID = 1L;
    
    public Two() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        out.println("This is second view");  
    }

}

在網頁看到的回應順序如下:


使用forward()
RequestDispatcher 有個forward()方法,呼叫的時候一樣傳入request和response兩個物件,將請求處理轉發給其他Servlet。

One.java
@WebServlet("/hello.do")
public class One extends HttpServlet {
 private static final long serialVersionUID = 1L;

    public One() {
    }
    
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  
  HelloModel model = new HelloModel();
  String name = request.getParameter("name");
  String message = model.sayMessage(name);
  RequestDispatcher disp = request.getRequestDispatcher("message.view");
  request.setAttribute("message", message);
  disp.forward(request, response);
   
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  doGet(request, response);
 }

}

HelloModel.java
public class HelloModel {
 
 private Map messages = new HashMap();
 
 public HelloModel() {
  messages.put("Eric","Hi");
  messages.put("Arch","Where are you?"); 
 }
 
 public String sayMessage(String user){
  return user+","+messages.get(user);
 }
}

Two.java
@WebServlet("/message.view")
public class Two extends HttpServlet {
 private static final long serialVersionUID = 1L;
       
    public Two() {
        super();
    }

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
  String template = "

%s

"; String html = String.format(template, (String)request.getAttribute("message")); response.getWriter().print(html); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }


執行時的結果


參考資料
  1. Servlet&JSP 教學手冊

2015年2月4日 星期三

中文編碼處理

中文編碼處理

當 Servlet 需要處理中文字時,那 Servlet 應該如何處理請求參數才能得到正確的中文字 ? 在編碼處理上基本有兩種情況處理分別是 GET 和 POST。

POST 請求參數編碼處理
如果客戶端沒有透過 Content-Type 設定編碼資訊 ( charset=UTF-8 ),那 HTTPServletRequest 的 getCharacterEncoding() 回傳值將會是 null,這時候容器將會使用預設編碼( ISO-8859-1 ) 處理,如果客戶端透過 UTF-8 發送非 ASCII 的請求參數,而 Servlet 直接使用 getParameter() 取得參數值,就會得到不正確的結果。

要正確的顯示中文字那可以使用 HttpServletRequest 的 setCharacterEncoding() 指定 POST 請求參數時使用的編碼。一定要在取得任何請求參數前執行 setCharacterEncoding() 才有作用。

GET 請求參數編碼處理
如果請求參數是以 GET 的方式傳遞那要處理 UTF-8 的編碼該如何呢? 我相信到這很多人應該會說用 setCharacterEncoding() 吧。那 setCharacterEncoding() 就能正確的處理中文字元嗎? 答案是 NO! NO! NO! ( 很重要所以說三次 ) 。在 API 文件對這個方法的定義以經說明清楚。

Overrides the name of the character encoding used in the body of this request

表示這方法只能在請求參數使用 POST 傳遞才有效(在 Tomcat 預設和沒有修改設定的情況). 既然 setCharacterEncoding() 不能對 GET 的請求參數進行處理 , 那還有其它方法處理嗎 ? 另外一個編碼的處理方式, 則透過 String 的 getBytes() 指定編碼取得 bytes array 在透過 String 重新建構正確的編碼.

例如瀏覽器使用 UTF-8 處理字元, Web Container 預設使用 ISO-8859-1 編碼, 正確處理編碼的方式是:
String name = request.getParameter("uname");
name = new String(name.getBytes("ISO-8859-1"),"UTF-8");

以下有一範例:

建立表單分別使用 GET 和 POST
<!DOCTYPE HTML>
<html>
<head>
<title>form_get</title>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5">
</head>
<body>
<FORM method="post" action="encoding">
  user : <input type = "text" name = "uname"><br><br>
  <button type="submit">Submit</button>
</FORM>
</body>
</html>

分別處理 GET 和 POST 編碼的 Servlet 如下:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(/encoding)
public class EncodingDemo extends HttpServlet {
 private static final long serialVersionUID = 1L;
       
    public EncodingDemo() {
        super();
    }

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  String name = request.getParameter("uname");
     name = new String(name.getBytes("BIG5"),"BIG5");
  System.out.println(name);
  
 }

 
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  request.setCharacterEncoding("BIG5");
  String name = request.getParameter("uname");
  System.out.println(name);
 }

}