Line for 3DS: Difference between revisions

From GameBrew
No edit summary
Line 4: Line 4:
|description=Line for 3DS.
|description=Line for 3DS.
|author=Core-2-Extreme
|author=Core-2-Extreme
|lastupdated=2021/01/07
|lastupdated=2024/01/30
|type=Other Apps
|type=Other Apps
|version=1.7.2
|version=2.0.0.1
|license=Mixed
|license=Mixed
|download=https://dlhb.gamebrew.org/3dshomebrews/linefor3ds.7z
|download=https://dlhb.gamebrew.org/3dshomebrews/linefor3ds.7z
|website=https://gbatemp.net/threads/v0-2-0-release-guide-line-for-3ds.539530
|website=https://gbatemp.net/threads/v0-2-0-release-guide-line-for-3ds.539530
|source=https://github.com/Core-2-Extreme/Line_for_3DS
|source=https://github.com/Core-2-Extreme/Line_for_3DS
|donation=https://github.com/Core-2-Extreme/Line_for_3DS/releases
}}
}}
Line for 3DS is a LINE client for the 3DS. Developed by LINE, it is an instant messaging, voice and video call application. It allows you to exchange text messages and make calls free of charge by transmitting information in the form of data.
Line for 3DS is a LINE client for the 3DS. Developed by LINE, it is an instant messaging, voice and video call application. It allows you to exchange text messages and make calls free of charge by transmitting information in the form of data.
Setup instructions can be found [https://github.com/Core-2-Extreme/Line_for_3DS/blob/v2.0.0/SETUP.md here].


==Features==
==Features==
Line 31: Line 34:
* Password (v0.3.0).
* Password (v0.3.0).
* Save log to SD card (v0.3.0).
* Save log to SD card (v0.3.0).
==User guide==
The stickers must be included this list [https://dlhb.gamebrew.org/3dshomebrew/sticker_list.pdf sticker_list.pdf].
Google drive URL will be sent (not embed).
===How to setup Google apps script===
* Open [https://developers.line.biz/en/ LINE Developers].
* Click log in > Log in with LINE account > Create.
* Type your name, click Create > Create a Messaging API channel.
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds5.png
* Type each item, click Create > Messaging API.
* Find Channel access token (long-lived), click Issue.
* Then copy Channel access token (long-lived).
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds6.png
* Open [https://www.google.com/intl/en_ALL/drive Google Drive].
* Login, then click New.
* Click Google sheets (1 time) > More > Google apps script (1 time).
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds7.png
* Open created Google sheets and copy sheet id (do NOT include "/").
* Open created Google apps script and Insert name ("Line main" in this guide).
* Copy the code below (click to expand) to "Line main", and add your sheet ID, access token, client (3DS's) account name (your favorite name) and script password (if you want).
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds8.png
{| role="presentation" class="wikitable mw-collapsible mw-collapsed"
| <strong>Code for Ver 1.7.0 ~ 2021/01/24</strong>
|-
|<pre>
var ACCESS_TOKEN = "Your acces token here";
var OPEN_SHEET_ID = "Your sheet id here";
var SCRIPT_PASSOWRD = "Your google apps script password here";
var CLIENT_NAME = "Your 3ds's account name here";
var gas_ver = 7;//Do **NOT** edit this value.
function log_save(message, user_name, write_sheet_name, group_or_user_name, pic_url, msg_id)
{
    var google_pic_url;
    var previous_pic_url;
    var response;
    var sheet_pos = 1;
    var spreadsheet = SpreadsheetApp.openById(OPEN_SHEET_ID);
    var write_sheet = spreadsheet.getSheetByName(write_sheet_name);
    if (!write_sheet)
    {
        spreadsheet.insertSheet(write_sheet_name);
        write_sheet = spreadsheet.getSheetByName(write_sheet_name);
    }
    sheet_pos = get_cache_pos(write_sheet);
    while (true)
    {
        var sheet_data = write_sheet.getRange("A" + sheet_pos).getValue();
        if (sheet_data == "")
        {
            write_sheet.getRange("A" + sheet_pos).setValue("" + user_name + " : " + message);
            write_sheet.getRange("C" + sheet_pos).setValue(msg_id);       
            break;
        }
        else
            sheet_pos++;
    }
    write_cache_data(write_sheet, (sheet_pos + 1), "B1");
    if (group_or_user_name != "Do not save")
        write_cache_data(write_sheet, group_or_user_name, "B2");
    if (pic_url != "Do not save")
    {
        previous_pic_url = get_previous_pic_url(write_sheet);
        if (previous_pic_url != pic_url)
        {
            response = UrlFetchApp.fetch(pic_url,
                {
                    'headers': {
                        'Content-Type': 'application/json; charset=UTF-8',
                        'Authorization': 'Bearer ' + ACCESS_TOKEN,
                    },
                    muteHttpExceptions: true,
                });
            if (response.getResponseCode() == 200)
            {
                google_pic_url = get_profile_pic_google_url(response, write_sheet_name);
                write_cache_data(write_sheet, google_pic_url, "B3");
                write_cache_data(write_sheet, pic_url, "B4");
            }
        }
    }
}
function unsend_msg(sheet_id, msg_id)
{
    var spreadsheet = SpreadsheetApp.openById(OPEN_SHEET_ID);
    var write_sheet = spreadsheet.getSheetByName(sheet_id);
    var sheet_pos = 1;
    if (!write_sheet)
    {
        spreadsheet.insertSheet(sheet_id);
        write_sheet = spreadsheet.getSheetByName(sheet_id);
    }
    while (true)
    {
        var sheet_data = write_sheet.getRange("C" + sheet_pos).getValue();
        if (sheet_data == msg_id)
        {
            write_sheet.getRange("A" + sheet_pos).setValue("*****This message was unsent by user*****");
            break;
        }
        else
            sheet_pos++;
    }
}
function check_folder_exist(input_folders, name)
{
    var folder;
    while (input_folders.hasNext())
    {
      folder = input_folders.next();
      if(name == folder.getName())
        return folder;
    }
    return undefined;
}
function log_read(id, num_of_logs)
{
  var sheet_offset = 1;
  var sheet_start = 1;
  var return_data;
  var sheet_data;
  var spreadsheet = SpreadsheetApp.openById(OPEN_SHEET_ID);
  var read_sheet = spreadsheet.getSheetByName(id);
  if(!read_sheet)
    return "No such sheet.\nPlease duoble check your ID.";
  sheet_start = get_cache_pos(read_sheet);
  while(true)
  {
    sheet_data = read_sheet.getRange("A" + sheet_start).getValue();
    if(sheet_data == "")
    {
      write_cache_data(read_sheet, sheet_start, "B1");
      if(sheet_start > num_of_logs)
        sheet_start = (sheet_start - num_of_logs);
      else
        sheet_start = 1;
      break;
    }
    else
      sheet_start++;
  }
  sheet_offset = sheet_start;
  sheet_data = read_sheet.getRange(sheet_start, 1, num_of_logs).getValues();
  return_data = "<0>";
  for(var i = 0; i < num_of_logs; i++)
  {
    if(sheet_data[i] == "")
      break;
    return_data += sheet_data[i];
    sheet_offset++;
  }
  return_data += "</0>";
  return_data += "<1>" + get_cache_name(read_sheet) + "</1>";
  return_data += "<2>" + get_google_pic_url(read_sheet) + "</2>";
  return_data += "<3>" + sheet_offset + "</3>";
  return_data += "<4>Success</4>";
  return return_data;
}
function get_cache_pos(sheet_object)
{
  var cached_sheet_pos = sheet_object.getRange("B1").getValue();
  var sheet_data;
  if(parseInt(cached_sheet_pos) > 0)
  {
    cached_sheet_pos = parseInt(cached_sheet_pos);
    sheet_data = sheet_object.getRange("A" + (cached_sheet_pos - 1)).getValue();
    if(sheet_data != "")
      return cached_sheet_pos;
  }
  return 1;
}
function get_cache_name(sheet_object)
{
  var cached_name = sheet_object.getRange("B2").getValue();
  return cached_name;
}
function get_google_pic_url(sheet_object)
{
    var cached_name = sheet_object.getRange("B3").getValue();
    return cached_name;
}
function get_previous_pic_url(sheet_object)
{
    var cached_name = sheet_object.getRange("B4").getValue();
    return cached_name;
}
function write_cache_data(sheet_object, cache_data, pos)
{
  sheet_object.getRange(pos).setValue(cache_data);
}
function http_get_content(request_id)
{
  var response;
  var url = "https://api-data.line.me/v2/bot/message/" + request_id + "/content";
  response = UrlFetchApp.fetch(url,
  {
  'headers': {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    muteHttpExceptions: true,
  });
  return response;
}
function http_get_user_name(id)
{
  var response;
  var url = 'https://api.line.me/v2/bot/profile/' + id;
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    muteHttpExceptions: true,
  });
  return response;
}
function http_get_group_user_name(id, group_id)
{
  var response;
  var url = "https://api.line.me/v2/bot/group/" + group_id + "/member/" + id + "/";
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    muteHttpExceptions: true,
  });
  return response;
}
function http_get_group_name(group_id)
{
  var response;
  var url = "https://api.line.me/v2/bot/group/" + group_id +"/summary/";
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    muteHttpExceptions: true,
  });
  return response;
}
function http_send_text_reply(reply_token, send_msg)
{
  var response;
  var url = 'https://api.line.me/v2/bot/message/reply/';
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': [{
        'type': 'text',
        'text': send_msg ,
      }],
      'notificationDisabled': 'true',
    }),
    muteHttpExceptions: true,
  });
  return response;
}
function http_send_text(id, send_msg)
{
  var response;
  var url = 'https://api.line.me/v2/bot/message/push/';
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
  "to": id,
  "messages":
      [{
          "text": send_msg,
          "type": "text",
      }],
      'notificationDisabled': 'false',
    }),
  muteHttpExceptions: true,
  });
  return response;
}
function http_send_sticker(id, package_id, sticker_id)
{
  var response;
  var url = 'https://api.line.me/v2/bot/message/push/';
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
  "to": id,
  "messages":
      [{
          "type": "sticker",
          "packageId": package_id,
          "stickerId": sticker_id,
      }],
      'notificationDisabled': 'false',
    }),
  muteHttpExceptions: true,
  });
  return response;
}
function http_send_image(id, img_url, preview_url)
{
  var response;
  var url = 'https://api.line.me/v2/bot/message/push/';
  response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
  "to": id,
  "messages":
      [{
          "type": "image",
          "originalContentUrl": img_url,
          "previewImageUrl": preview_url,
      }],
      'notificationDisabled': 'false',
    }),
  muteHttpExceptions: true,
  });
  return response;
}
function send_msg(id, send_message, time)
{
  var cache;
  var response;
  var return_message = "Success";
  response = http_send_text(id, send_message);
  if(response.getResponseCode() != 200)
  {
    cache = "***The message failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message + "*** ";
    cache += send_message;
    send_message = cache;
    return_message = "The message failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
  }
  send_message += "(" + time + ")";
  log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
  return return_message;
}
function send_sticker(id, package_id, sticker_id, time)
{
  var response;
  var return_message = "Success";
  var send_message = "";
  response = http_send_sticker(id, package_id, sticker_id);
  if(response.getResponseCode() != 200)
  {
  send_message = "***The sticker failed to send. Status code = " + response.getResponseCode() + " " + JSON.parse(response.getContentText()).message + "*** ";
  return_message = "The sticker failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
  }
  send_message += "<sticker>" + sticker_id + "</sticker>(" + time + ")";
  log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
  return return_message;
}
function get_cache_data(id, cache_name, last_data)
{
  var byte_data;
  var cache_data = "";
  var cache_data_1 = "";
  var cache_file;
  var cache_folder;
  var exist_cache_folders;
  var exist_folders;
  var exist_sub_folders;
  var folder;
  var folder_name = "Line_contents";
  var sub_folder;
  exist_folders = DriveApp.searchFolders("'me' in owners");
  folder = check_folder_exist(exist_folders, folder_name);
  if(folder == undefined)
    return undefined;
  exist_sub_folders = folder.searchFolders("'me' in owners");
  sub_folder = check_folder_exist(exist_sub_folders, id);
  if(sub_folder == undefined)
    return undefined;
  exist_cache_folders = sub_folder.searchFolders("'me' in owners");
  cache_folder = check_folder_exist(exist_cache_folders, cache_name);
  if(cache_folder == undefined)
    return undefined;
  for(var i = 0; i < 4096; i++)
  {
    try
    {
      cache_file = cache_folder.getFilesByName(i).next();
      cache_data += cache_file.getBlob().getDataAsString();
    }
    catch(error)
    {
      break;
    }
  }
  cache_data += last_data;
  cache_folder.setTrashed(true);
  return cache_data;
}
function upload_content(id, data, count, cache_name, last, time, data_type)
{
  var cache_data;
  var cache_folder;
  var exist_cache_folders;
  var exist_folders;
  var exist_sub_folders;
  var folder;
  var folder_name = "Line_contents";
  var part_of_content;
  var part_of_content_data;
  var return_message = "Success";
  var send_message;
  var sub_folder;
  if(count == 0 && last == "true")
    return_message = send_content(id, cache_name, time, data, data_type);
  else if(last == "true")
  {
      cache_data = get_cache_data(id, cache_name, data);
      if(cache_data == undefined)
      {
        send_message = "***The content failed to send. The cache folder does not exist.*** (" + time + ")";
        log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
        return_message = "The content failed to send.\nThe cache folder does not exist.";
      }
      else
        return_message = send_content(id, cache_name, time, cache_data, data_type);
  }
  else
  {
    exist_folders = DriveApp.searchFolders("'me' in owners");
    folder = check_folder_exist(exist_folders, folder_name);
    if(folder == undefined)
      folder = DriveApp.createFolder(folder_name);
    exist_sub_folders = folder.searchFolders("'me' in owners");
    sub_folder = check_folder_exist(exist_sub_folders, id);
    if(sub_folder == undefined)
      sub_folder = folder.createFolder(id);
    exist_cache_folders = sub_folder.searchFolders("'me' in owners");
    cache_folder = check_folder_exist(exist_cache_folders, cache_name);
    if(cache_folder == undefined)
      cache_folder = sub_folder.createFolder(cache_name);
    else if(!(cache_folder == undefined) && count == 0)
    {
      cache_folder.setTrashed(true);
      cache_folder = sub_folder.createFolder(cache_name);
    }
    part_of_content_data = Utilities.newBlob(data).setName(count);
    part_of_content = cache_folder.createFile(part_of_content_data);
  }
  return return_message;
}
function send_content(id, cache_name, time, encoded_data, data_type)
{
  var content;
  var content_data;
  var content_type;
  var content_url = "";
  var exist_folders;
  var exist_sub_folders;
  var folder;
  var folder_name = "Line_contents";
  var preview_content_url = "";
  var response;
  var return_message = "Success";
  var send_message = "";
  var sub_folder;
  exist_folders = DriveApp.searchFolders("'me' in owners");
  folder = check_folder_exist(exist_folders, folder_name);
  if(folder == undefined)
    folder = DriveApp.createFolder(folder_name);
  exist_sub_folders = folder.searchFolders("'me' in owners");
  sub_folder = check_folder_exist(exist_sub_folders, id);
  if(sub_folder == undefined)
    sub_folder = folder.createFolder(id);
  try
  {
    content_data = Utilities.newBlob(Utilities.base64Decode(encoded_data)).setName(cache_name);
  }
  catch (e)
  {
    return_message = "The content failed to send.\nCouldn't decode base64 data.";
    return return_message;
  }
  content = sub_folder.createFile(content_data);
  content_type = content.getMimeType();
  if(content_type == MimeType.BMP || content_type == MimeType.GIF || content_type == MimeType.JPEG || content_type == MimeType.PNG || content_type == MimeType.SVG)
  {
    content.setTrashed(true);
    folder_name = "Line_images";
    exist_folders = DriveApp.searchFolders("'me' in owners");
    folder = check_folder_exist(exist_folders, folder_name);
    if(folder == undefined)
      folder = DriveApp.createFolder(folder_name);
    exist_sub_folders = folder.searchFolders("'me' in owners");
    sub_folder = check_folder_exist(exist_sub_folders, id);
    if(sub_folder == undefined)
      sub_folder = folder.createFolder(id);
    content = sub_folder.createFile(content_data);
    content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    content_url = "https://lh3.googleusercontent.com/d/" + content.getId() + "=w16383-h16383";
    preview_content_url = "https://lh3.googleusercontent.com/d/" + content.getId() + "=w240-h240";
    response = http_send_image(id, content_url, preview_content_url);
    if(response.getResponseCode() != 200)
    {
      send_message = "***The image failed to send. Status code = " + response.getResponseCode() + " " + JSON.parse(response.getContentText()).message + "*** ";
      return_message = "The image failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
    }
    send_message += "<image_url>" + content_url + "</image_url>(" + time + ")";
    log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
    return return_message;
  }
  else if(data_type == "audio")
  {
    content.setTrashed(true);
    folder_name = "Line_audio";
    exist_folders = DriveApp.searchFolders("'me' in owners");
    folder = check_folder_exist(exist_folders, folder_name);
    if(folder == undefined)
      folder = DriveApp.createFolder(folder_name);
    exist_sub_folders = folder.searchFolders("'me' in owners");
    sub_folder = check_folder_exist(exist_sub_folders, id);
    if(sub_folder == undefined)
      sub_folder = folder.createFolder(id);
    content = sub_folder.createFile(content_data);
    content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    content_url = "https://docs.google.com/uc?id=" + content.getId();
    response = http_send_text(id, content_url + " (audio)");
    if(response.getResponseCode() != 200)
    {
      send_message = "***The audio failed to send. Status code = " + response.getResponseCode() + " " + JSON.parse(response.getContentText()).message + "*** ";
      return_message = "The audio failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
    }
    content_url = "https://drive.google.com/uc?export=download&id=" + content.getId();
    send_message += "<audio_url>" + content_url + "</audio_url>(" + time + ")";
    log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
    return return_message;
  }
  else if(data_type == "video")
  {
    content.setTrashed(true);
    folder_name = "Line_videos";
    exist_folders = DriveApp.searchFolders("'me' in owners");
    folder = check_folder_exist(exist_folders, folder_name);
    if(folder == undefined)
      folder = DriveApp.createFolder(folder_name);
    exist_sub_folders = folder.searchFolders("'me' in owners");
    sub_folder = check_folder_exist(exist_sub_folders, id);
    if(sub_folder == undefined)
      sub_folder = folder.createFolder(id);
    content = sub_folder.createFile(content_data);
    content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    content_url = "https://docs.google.com/uc?id=" + content.getId();
    response = http_send_text(id, content_url);
    if(response.getResponseCode() != 200)
    {
      send_message = "***The video failed to send. Status code = " + response.getResponseCode() + " " + JSON.parse(response.getContentText()).message + "*** ";
      return_message = "The video failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
    }
    content_url = "https://drive.google.com/uc?export=download&id=" + content.getId();
    send_message += "<video_url>" + content_url + "</video_url>(" + time + ")";
    log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
    return return_message;
  }
  else
  {
    content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    content_url = "https://drive.google.com/file/d/" + content.getId() + "/view";
    response = http_send_text(id, content_url);
    if(response.getResponseCode() != 200)
    {
      send_message = "***The content failed to send. Status code = " + response.getResponseCode() + " " + JSON.parse(response.getContentText()).message + "*** ";
      return_message = "The content failed to send. Status code = " + response.getResponseCode() + "\n" + JSON.parse(response.getContentText()).message;
    }
    content_url = "https://drive.google.com/uc?export=download&id=" + content.getId();
    send_message += "<content_url>" + content_url + "</content_url>(" + time + ")";
    log_save(send_message, CLIENT_NAME, id, "Do not save", "Do not save", "0");
    return return_message;
  }
}
function get_profile_pic_google_url(http_response, group_or_user_id)
{
    var content_data;
    var content;
    var folder_name = "Line_images";
    var folder;
    var sub_folder;
    var exist_folders;
    var exist_sub_folders;
    var url = "";
    exist_folders = DriveApp.searchFolders("'me' in owners");
    folder = check_folder_exist(exist_folders, folder_name);
    if (folder == undefined)
        folder = DriveApp.createFolder(folder_name);
    exist_sub_folders = folder.searchFolders("'me' in owners");
    sub_folder = check_folder_exist(exist_sub_folders, group_or_user_id);
    if (sub_folder == undefined)
        sub_folder = folder.createFolder(group_or_user_id);
    content_data = http_response.getBlob().getAs("image/jpeg").setName("icon");
    content = sub_folder.createFile(content_data);
    content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
    url = "https://lh3.googleusercontent.com/d/" + content.getId() + "=w32-h32";
    return url;
}
function get_content_url(request_id, group_or_user_id, type)
{
  var content_data;
  var content;
  var exist_folders;
  var exist_sub_folders;
  var folder_name;
  var folder;
  var sub_folder;
  var response;
  var url = "";
  if(type == "image")
    folder_name = "Line_images";
  else if(type == "audio")
    folder_name = "Line_audio";
  else if(type == "video")
    folder_name = "Line_videos";
  else if(type == "file")
    folder_name = "Line_contens";
  exist_folders = DriveApp.searchFolders("'me' in owners");
  folder = check_folder_exist(exist_folders, folder_name);
  if(folder  == undefined)
    folder = DriveApp.createFolder(folder_name);
  exist_sub_folders = folder.searchFolders("'me' in owners");
  sub_folder = check_folder_exist(exist_sub_folders, group_or_user_id);
  if(sub_folder  == undefined)
    sub_folder = folder.createFolder(group_or_user_id);
  response = http_get_content(request_id);
  if(response.getResponseCode() != 200)
  {
    if(type == "image")
      url = "***Couldn't generate an image URL. Status code = " + response.getResponseCode() + "*** ";
    else if(type == "audio")
      url = "***Couldn't generate a audio URL. Status code = " + response.getResponseCode() + "*** ";
    else if(type == "video")
      url = "***Couldn't generate a video URL. Status code = " + response.getResponseCode() + "*** ";
    else if(type == "file")
      url = "***Couldn't generate a file URL. Status code = " + response.getResponseCode() + "*** ";
  }
  else
  {
    if(type == "image")
    {
      content_data = response.getBlob().getAs("image/jpeg").setName(request_id);
      content = sub_folder.createFile(content_data);
      content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
      url = "https://lh3.googleusercontent.com/d/" + content.getId() + "=w16383-h16383";
    }
    else if(type == "audio" || type == "video" || type == "file")
    {
      content_data = response.getBlob().setName(request_id);
      content = sub_folder.createFile(content_data);
      content.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);
      url = "https://drive.google.com/uc?export=download&id=" + content.getId();
    }
  }
  return url;
}
function receive_msg_from_line(user_message, user_id, group_id, reply_token, time, msg_id)
{
  var group_name;
  var picture_url = [ "", ""];
  var response = [ "", ""];
  var send_msg;
  var user_name;
  if(group_id == "Unknown")
    response[0] = http_get_user_name(user_id);
  else
  {
    response[0] = http_get_group_user_name(user_id, group_id);
    response[1] = http_get_group_name(group_id);
    if(response[1].getResponseCode() != 200)
      group_name = "Unknown";
    else
    {
      group_name = JSON.parse(response[1]).groupName;
      picture_url[1] = JSON.parse(response[1]).pictureUrl;
    }
  }
  if(response[0].getResponseCode() != 200)
    user_name = "Unknown";
  else
  {
    user_name = JSON.parse(response[0]).displayName;
    picture_url[0] = JSON.parse(response[0]).pictureUrl;
  }
  if(user_message == "getid" || user_message == "getgroupid")
  {
    if(user_message == "getid")
      send_msg = user_id;
    else if(user_message == "getgroupid")
      send_msg = group_id;
    response[0] = http_send_text_reply(reply_token, send_msg);
    send_msg = "<id>" + send_msg + "</id>";
    if(response[0].getResponseCode() != 200)
      send_msg += "***The message failed to send. Status code = " + response[0].getResponseCode() + " " + JSON.parse(response[0].getContentText()).message + "*** ";
    send_msg += "(" + time + ")";
    if(user_message == "getid")
    {
      log_save(send_msg, user_name, "IDs", "BOT", "Do not save", "0");
      user_message = "*****Added by user*****";
    }
    else if(user_message == "getgroupid")
    {
      log_save(send_msg, group_name, "IDs", "BOT", "Do not save", "0");
      user_message = "*****Invited by user*****";
    }
    user_name = CLIENT_NAME;
    msg_id = 0;
  }
  else if(user_message == "*****Removed by user*****" || user_message == "*****Blocked by user*****" || user_message == "*****Joined*****" || user_message == "*****Left*****")
  {
    msg_id = 0;
    if(user_message == "*****Removed by user*****" || user_message == "*****Blocked by user*****")
      user_name = CLIENT_NAME;
  }
  user_message += "(" + time + ")";
  if(group_id == "Unknown")
    log_save(user_message, user_name, user_id, user_name, picture_url[0], msg_id);
  else
    log_save(user_message, user_name, group_id, group_name, picture_url[1], msg_id);
}
function doPost(post_data)
{
  var time = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'MM/dd hh:mm:ss');
  var data_type;
  var result;
  var client_auth = "Unknown";
  var client_gas_ver = "Unknown";
  var id = "Unknown";
  var last = "";
  var lock = LockService.getScriptLock();
  var lock_result;
  var logs = 100;
  try
  {
    data_type = JSON.parse(post_data.postData.contents).type;
  }
  catch(e)
  {
  }
  for(var i = 0; i < 1000; i++)
  {
    lock_result = lock.tryLock(1000);
    if(lock_result)
      break;
    Utilities.sleep(1000);
  }
  if(data_type == undefined)
  {
    for(var i = 0; i < 50; i++)
    {
      var msg_id = "";
      var user_message = "";
      var user_id = "";
      var group_id = "";
      var reply_token = "";
      var msg_type = "";
      var type = "";
 
      try
      {
        type = JSON.parse(post_data.postData.contents).events[i].type;
        user_id = JSON.parse(post_data.postData.contents).events[i].source.userId;
        group_id = JSON.parse(post_data.postData.contents).events[i].source.groupId;
        reply_token = JSON.parse(post_data.postData.contents).events[i].replyToken;
        if(type == "unsend")
          msg_id = JSON.parse(post_data.postData.contents).events[i].unsend.messageId;
        else if(type == "message")
        {
          msg_id = JSON.parse(post_data.postData.contents).events[i].message.id;
          user_message = JSON.parse(post_data.postData.contents).events[i].message.text;
          msg_type = JSON.parse(post_data.postData.contents).events[i].message.type;             
        }
      }
      catch(e)
      {
        break;
      }
      if(user_id == undefined)
        user_id = "Unknown";
      if(group_id == undefined)
        group_id = "Unknown";
      if(type == "follow")
        user_message = "getid";
      else if(type == "join")
        user_message = "getgroupid";
      else if(type == "leave")
        user_message = "*****Removed by user*****";
      else if(type == "unfollow")
        user_message = "*****Blocked by user*****";
      else if(type == "memberJoined")
      {
        user_message = "*****Joined*****";
        user_id = JSON.parse(post_data.postData.contents).events[i].joined.members[0].userId;
      }
      else if(type == "memberLeft")
      {
        user_message = "*****Left*****";
        user_id = JSON.parse(post_data.postData.contents).events[i].left.members[0].userId;
      }
      else if(type == "unsend")
      {
        if(group_id == "Unknown")
          unsend_msg(user_id, msg_id);
        else
          unsend_msg(group_id, msg_id);
      }
      else if(msg_type == "sticker")
      {
        var sticker_id  = JSON.parse(post_data.postData.contents).events[i].message.stickerId;
        user_message = "<sticker>" + sticker_id + "</sticker>";
      }
      else if(msg_type == "image")
      {     
        if(group_id == "Unknown")
          user_message = "<image_url>" + get_content_url(msg_id, user_id, msg_type) + "</image_url>";
        else
          user_message = "<image_url>" + get_content_url(msg_id, group_id, msg_type) + "</image_url>";
      }
      else if(msg_type == "audio")
      {
      if(group_id == "Unknown")
        user_message = "<audio_url>" + get_content_url(msg_id, user_id, msg_type) + "</audio_url>";
      else
        user_message = "<audio_url>" + get_content_url(msg_id, group_id, msg_type) + "</audio_url>";
      }
      else if(msg_type == "video")
      {
      if(group_id == "Unknown")
        user_message = "<video_url>" + get_content_url(msg_id, user_id, msg_type) + "</video_url>";
      else
        user_message = "<video_url>" + get_content_url(msg_id, group_id, msg_type) + "</video_url>";
      }
      else if(msg_type == "file")
      {
      if(group_id == "Unknown")
        user_message = "<file_url>" + get_content_url(msg_id, user_id, msg_type) + "</file_url>";
      else
        user_message = "<file_url>" + get_content_url(msg_id, group_id, msg_type) + "</file_url>";
      }
      else if(user_message == undefined)
        user_message += " : " + msg_type + " " + msg_id;
      if(!(type == "unsend"))
        receive_msg_from_line(user_message, user_id, group_id, reply_token, time, msg_id);
    }
    lock.releaseLock();
    return;
  }
  try
  {
    client_auth = JSON.parse(post_data.postData.contents).auth;
    client_gas_ver = JSON.parse(post_data.postData.contents).gas_ver;
    id = JSON.parse(post_data.postData.contents).id;
  }
  catch(e)
  {
  }
  if(SCRIPT_PASSOWRD == client_auth)
  {
    if(gas_ver == client_gas_ver)
    {
      if(data_type == "send_text")
      {
        var send_message = JSON.parse(post_data.postData.contents).message;
        result = send_msg(id, send_message, time);
      }
      else if(data_type == "send_sticker")
      {
        var package_id = JSON.parse(post_data.postData.contents).package_id;
        var sticker_id = JSON.parse(post_data.postData.contents).sticker_id;
        result = send_sticker(id, package_id, sticker_id, time);
      }
      else if(data_type == "upload_content")
      {
        var content_data = JSON.parse(post_data.postData.contents).content_data;
        var count = JSON.parse(post_data.postData.contents).count;
        var name = JSON.parse(post_data.postData.contents).name;
        var sub_data_type = "";
        last = JSON.parse(post_data.postData.contents).last;
        if(last == "true")
          sub_data_type = JSON.parse(post_data.postData.contents).data_type;
   
        result = upload_content(id, content_data, count, name, last, time, sub_data_type);
      }
      else
        result = "Unknown message type.";
   
      if(result == "Success" && ((data_type == "upload_content" && last == "true") || data_type == "send_sticker" || data_type == "send_text"))
      {
        logs = JSON.parse(post_data.postData.contents).logs;
        result = log_read(id, logs);
      }
    }
    else
      result = "Google apps script version does not match. Server's gas\nver is " + gas_ver + ", but 3DS's gas ver is " + client_gas_ver + ". Please use the same version.";
  }
  else
    result = "Auth failed. Please set correct password.";
  lock.releaseLock();
  return ContentService.createTextOutput(result);
}
function doGet(post_data)
{
  var data = "";
  var client_auth = "";
  var client_gas_ver = "";
  var num_of_logs = "";
  var id = "";
  var lock = LockService.getScriptLock();
  var lock_result = lock.tryLock(1000);
  while(!lock_result)
  {
    Utilities.sleep(1000);
    lock_result = lock.tryLock(1000);
  }
  try
  {
    client_auth = post_data.parameter.script_auth;
    client_gas_ver = post_data.parameter.gas_ver;
    num_of_logs = post_data.parameter.logs;
    id = post_data.parameter.id;
  }
  catch(e)
  {
  }
  if(client_auth == undefined)
    client_auth = "";
  if(client_gas_ver == undefined)
    client_gas_ver = -1;
  if(num_of_logs == undefined)
    num_of_logs = 300;
  if(id == undefined)
    id = "unknown";
  if(SCRIPT_PASSOWRD == client_auth)
  {
    if(gas_ver == client_gas_ver)
      data = log_read(id, num_of_logs);
    else
      data = "Google apps script version does not match. Server's gas\nver is " + gas_ver + ", but 3DS's gas ver is " + client_gas_ver + ". Please use the same version.";
  }
  else
    data = "Auth failed. Please set correct password.";
  lock.releaseLock();
  return ContentService.createTextOutput(data);
}
</pre>
|-
|}
* Then click Publish > Deploy as web app using the following settings:
** Project version - New.
** Execute the app as - Me (***@gmail.com).
** Who has access to the app - Anyone, even anonymous.
* Finally click Deploy.
* Next click Review permissions and select your google account.
* (If "This app isn't verified/You should avoid this app" shown, click Advanced > Go to Line main (unsafe) > Allow.)
* Copy this URL and save.
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds9.png
* Back to Line Messaging API.
* Find Webhook URL and click edit.
* Do NOT type short url here.
* Type your Google apps script URL, click Update.
* Turn on Use webhook, click Verify to check webhook works fine.
** Check result and if "success" wasn't displayed, double check your Google apps script URL.
** (Delete whole url once and re-typing is also good idea.)
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds10.png
* Go to https://free-url-shortener.rb.gy or https://tiny.cc or https://tinyurl.com.
* Then shorten your Google apps script URL.
* Your URL should be http://tinyurl.com/*** or http://tiny.cc/*** or http://rb.gy/***
* This short URL is needed by next section - How to setup 3DS.
===How to setup 3DS===
* Start the application and select Line.
* Set your password (if you don't want use password, enter as empty).
* Press L+R button (or tap Change main URL (short)) and type your short url.
* You MUST type short URL with http:// NOT https:// (don't know why but https:// won't work.).
* This method is compatiable with Ver 1.5.0~1.7.0.
===How to add friend===
On PC or phone (or android emulator):
* Find Bot information.
* And then scan your bot's QR code, or search friends by BOT basic ID.
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds11.png
On 3DS (this guide compatible with Ver 1.6.0~1.7.0):
* Press Y button (or tap Add new ID) and type IDs.
* Select IDs > Tap Receive and press B button (or tap log update).
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds12.png
If you get No such sheet error:
* Send "getid" to your 3DS account from your phone/PC account.
* Then try again.
* You'll see touch to add this ID and tap it.
https://dlhb.gamebrew.org/3dshomebrews/linefor3ds13.png


==Screenshots==
==Screenshots==
Line 1,162: Line 44:


==Changelog==
==Changelog==
'''Ver 1.7.2'''
'''v2.0.0.1 2024/01/30'''
* Line:
** Significant changes for internal code, so you need to setup your Google Apps Script again.
** [https://github.com/Core-2-Extreme/Line_for_3DS/blob/v2.0.0/SETUP.md Setup instructions] are updated.
** 3DS:
*** Chat log wrapping feature has been improved a lot.
*** Auto update feature (for chat logs) are removed temporary.
*** Clear cache feature (for downloaded files) are removed temporary.
** Google Apps Script:
*** Self tests are added so that it is much easier to trouble shoot.
* Camera:
** Supported QR code scan (currently, only for Google Apps Script setup).
* Video player:
** Updated to v1.5.3 (see [https://github.com/Core-2-Extreme/Video_player_for_3DS/blob/v1.5.3#v153 Video player for 3DS patch note] for details).
* Overall:
** Application folder has been changed from <code>/Line/</code> to <code>/3ds/Line/</code> (old folder won't be deleted automatically, but you can delete it if you don't need it).
** Sub application icon has been added.
 
'''v2.0.0 2023/01/27'''
* Line:
** Significant changes for internal code, so you need to setup your Google Apps Script again.
** [https://github.com/Core-2-Extreme/Line_for_3DS/blob/v2.0.0/SETUP.md Setup instructions] are updated.
** 3DS:
*** Chat log wrapping feature has been improved a lot.
*** Auto update feature (for chat logs) are removed temporary.
*** Clear cache feature (for downloaded files) are removed temporary.
** Google Apps Script:
*** Self tests are added so that it is much easier to trouble shoot.
* Camera:
** Supported QR code scan (currently, only for Google Apps Script setup).
* Video player:
** Updated to v1.5.3 (see [https://github.com/Core-2-Extreme/Video_player_for_3DS/blob/v1.5.3#v153 Video player for 3DS patch note] for details).
* Overall:
** Application folder has been changed from <code>/Line/</code> to <code>/3ds/Line/</code> (old folder won't be deleted automatically, but you can delete it if you don't need it).
** Sub application icon has been added.
 
'''v1.7.2 2021/01/07'''
* [Line] Fixed some bugs.
* [Line] Fixed some bugs.
* [Vid] Added debug infomation.
* [Vid] Added debug infomation.
Line 1,168: Line 86:
* [App] Some minor update to better user experience.
* [App] Some minor update to better user experience.


'''Ver 1.7.1'''
'''v1.7.1 2020/12/13'''
* [Line] Added Supported download all files.
* [Line] Added Supported download all files.
* [Mup/Vid] Added Additional formats support(.ogg).
* [Mup/Vid] Added Additional formats support(.ogg).
* [App] Some minor update to better user experience.
* [App] Some minor update to better user experience.


'''Ver 1.7.0'''
'''v1.7.0 2020/12/03'''
* [App] Fixed Fonts was improved.
* [App] Fixed Fonts was improved.
* [App] Added Video player.
* [App] Added Video player.
Line 1,181: Line 99:
* [App] Some minor update to better user experience.
* [App] Some minor update to better user experience.


'''V1.3.1'''
'''v1.6.0 2020/09/12'''
* [App] Ver 1.3.1.
* [Line] Fixed- The app will crash if use 'send a file' function.
* [Line] Fixed- Improved 'send a file' function processing speed.
* [Line] Added- Search function.
* [Line] Added- Now, you can receive up to 4000 chat logs.
* [Music player] Fixed- Improved mp3 play quality.
* [Music player] Added- Seek function.
* [Explorer] Added- Now, file will be sorted.
 
'''v1.5.2 2020/07/14'''
* [Line] Fixed- Adjust some UI.
* [Camera] Fixed- Some settings will be reset after restarting the camera.
* [Mic] Fixed- The app will crash if couldn't allocate memory(out of memory).
* [Music player] Fixed- The app sometimes crash when try playing .mp3 files.
* [Music player] Added- Shuffle function.
 
'''v1.5.1 2020/06/30'''
* [App] Fixed- The message will not be displayed correctly if the Line folder does not exist.
* [Line] Fixed- The message will not be sent if double quote or backslash was included.
* [Line] Added- Now, you can include new lines in the message.
* [Music player] Fixed- A memory leak occurs when playing .mp3 files.
 
'''v1.5.0 2020/06/22'''
* [App] Added- Camera, Mic and Music player.
* [App] Build with the latest version of ctrlib(v1.9.0)
* [Line] Fixed- UI design.
* [Line] Fixed- Made setup steps easier.
* [Line] Added- Display user/group name and user/group icon.
* [Google translation] Fixed- Some wrong control.
* [Speed test] Fixed- Some text color.
* [Image viewer] Fixed- Some crash.
* [Setting menu] Fixed- UI design.
 
'''v1.4.2 2020/05/11'''
* [App] Fixed- App no longer needs CFW to launch.
 
'''v1.4.1 2020/05/05'''
* [Line] Fixed- Some wrong messages.
* [Line] Fixed- The app will crash if unsupported sticker was sent.
* [Google translation] Fixed- The translation result will be displayed incorrectly in some case.
* [Google translation] Added- Now, you can translate into over 100 languages.(⁂Some languages(character) will be garbale.)
 
'''v1.4.0 2020/04/26'''
* [Line] Added- send a file function.
* [Image viewer] Added- view files on SD card.
* [GAS] Fixed- The problem of losing messages from being sent too quickly have been resolved.
 
'''v1.3.1 2020/03/31'''
* [Line] Fixed Common occurring [FSUSER_OpenArchive failed] error has been solved.
* [Line] Fixed Common occurring [FSUSER_OpenArchive failed] error has been solved.
* [Line] Fixed Image display now activated by touching the message.
* [Line] Fixed Image display now activated by touching the message.
Line 1,188: Line 152:
* [Image viewer] Fixed Image display speed now improved.
* [Image viewer] Fixed Image display speed now improved.


'''v1.2.0'''
'''v1.3.0 2020/03/25'''
* [Line] Added- sticker send and receive.
 
'''v1.2.2 2020/05/17'''
* [Setting] Added- RAM allocation control menu.
* [Line] Fixed- Now, some errors have been handled.
* [Image viewer] Fixed- Now, some errors have been handled.
* [Speedtest] Fixed- Now, some errors have been handled.
 
'''v1.2.1 2020/03/03'''
* [App] Changed title id to 000400000EC95000.
* [Setting] Added- Fonts setting.
* [Setting] Added- Scroll bar.
* [Line] Fixed- Auto update was destroyed.
* [Line] Added- Hide ID.
* [Line] Added- Scroll bar.
* [Image viewer] Fixed- clipboard.
* [Pull requests] Fixed- [https://github.com/Core-2-Extreme/Line_for_3DS/pull/3 #3].
 
'''v1.2.0 2020/01/17'''
* Fix Some crashes.
* Fix Some crashes.
* Fix Some settings do not work.
* Fix Some settings do not work.
* Fix Log download (GAS processing) speed has been improved(GAS update required).
* Fix Log download (GAS processing) speed has been improved(GAS update required).
* Add Image viewer.
* Add Image viewer.
[https://github.com/Core-2-Extreme/Line_for_3DS/releases Release notes.]


==External links==
==External links==

Revision as of 05:58, 25 February 2024

Line for 3DS
Linefor3ds2.png
General
AuthorCore-2-Extreme
TypeOther Apps
Version2.0.0.1
LicenseMixed
Last Updated2024/01/30
Links
Download
Website
Source
Support Author

Line for 3DS is a LINE client for the 3DS. Developed by LINE, it is an instant messaging, voice and video call application. It allows you to exchange text messages and make calls free of charge by transmitting information in the form of data.

Setup instructions can be found here.

Features

  • Send text.
  • Send image (v1.4.0).
  • Send video(*2) (v1.4.0).
  • Send sound(*2) (v1.4.0).
  • Send sticker (*1) (v1.3.0).
  • Receive text.
  • Receive (View) image (v1.2.0).
  • Receive (Play) video (v1.7.0).
  • Receive (Play) sound (v1.7.0).
  • Receive (View) sticker(*1) (v1.3.0).
  • View old log (~v1.5.2 Max 300 logs) (v1.6.0~ Max 4000 logs).
  • Auto update (v0.2.0).
  • Group chat.
  • Night mode (v0.2.0).
  • Password (v0.3.0).
  • Save log to SD card (v0.3.0).

Screenshots

linefor3ds3.pnglinefor3ds4.png

Media

Line for 3DS (Line for 3DS)

Changelog

v2.0.0.1 2024/01/30

  • Line:
    • Significant changes for internal code, so you need to setup your Google Apps Script again.
    • Setup instructions are updated.
    • 3DS:
      • Chat log wrapping feature has been improved a lot.
      • Auto update feature (for chat logs) are removed temporary.
      • Clear cache feature (for downloaded files) are removed temporary.
    • Google Apps Script:
      • Self tests are added so that it is much easier to trouble shoot.
  • Camera:
    • Supported QR code scan (currently, only for Google Apps Script setup).
  • Video player:
  • Overall:
    • Application folder has been changed from /Line/ to /3ds/Line/ (old folder won't be deleted automatically, but you can delete it if you don't need it).
    • Sub application icon has been added.

v2.0.0 2023/01/27

  • Line:
    • Significant changes for internal code, so you need to setup your Google Apps Script again.
    • Setup instructions are updated.
    • 3DS:
      • Chat log wrapping feature has been improved a lot.
      • Auto update feature (for chat logs) are removed temporary.
      • Clear cache feature (for downloaded files) are removed temporary.
    • Google Apps Script:
      • Self tests are added so that it is much easier to trouble shoot.
  • Camera:
    • Supported QR code scan (currently, only for Google Apps Script setup).
  • Video player:
  • Overall:
    • Application folder has been changed from /Line/ to /3ds/Line/ (old folder won't be deleted automatically, but you can delete it if you don't need it).
    • Sub application icon has been added.

v1.7.2 2021/01/07

  • [Line] Fixed some bugs.
  • [Vid] Added debug infomation.
  • [Vid] Changed initial image size and position.
  • [App] Some minor update to better user experience.

v1.7.1 2020/12/13

  • [Line] Added Supported download all files.
  • [Mup/Vid] Added Additional formats support(.ogg).
  • [App] Some minor update to better user experience.

v1.7.0 2020/12/03

  • [App] Fixed Fonts was improved.
  • [App] Added Video player.
  • [Line] Added Now, you can play audio and video sent by user.
  • [Cam] Fixed Framerate was improved.
  • [Mup] Added Additional formats (like aac) are supported.
  • [App] Some minor update to better user experience.

v1.6.0 2020/09/12

  • [Line] Fixed- The app will crash if use 'send a file' function.
  • [Line] Fixed- Improved 'send a file' function processing speed.
  • [Line] Added- Search function.
  • [Line] Added- Now, you can receive up to 4000 chat logs.
  • [Music player] Fixed- Improved mp3 play quality.
  • [Music player] Added- Seek function.
  • [Explorer] Added- Now, file will be sorted.

v1.5.2 2020/07/14

  • [Line] Fixed- Adjust some UI.
  • [Camera] Fixed- Some settings will be reset after restarting the camera.
  • [Mic] Fixed- The app will crash if couldn't allocate memory(out of memory).
  • [Music player] Fixed- The app sometimes crash when try playing .mp3 files.
  • [Music player] Added- Shuffle function.

v1.5.1 2020/06/30

  • [App] Fixed- The message will not be displayed correctly if the Line folder does not exist.
  • [Line] Fixed- The message will not be sent if double quote or backslash was included.
  • [Line] Added- Now, you can include new lines in the message.
  • [Music player] Fixed- A memory leak occurs when playing .mp3 files.

v1.5.0 2020/06/22

  • [App] Added- Camera, Mic and Music player.
  • [App] Build with the latest version of ctrlib(v1.9.0)
  • [Line] Fixed- UI design.
  • [Line] Fixed- Made setup steps easier.
  • [Line] Added- Display user/group name and user/group icon.
  • [Google translation] Fixed- Some wrong control.
  • [Speed test] Fixed- Some text color.
  • [Image viewer] Fixed- Some crash.
  • [Setting menu] Fixed- UI design.

v1.4.2 2020/05/11

  • [App] Fixed- App no longer needs CFW to launch.

v1.4.1 2020/05/05

  • [Line] Fixed- Some wrong messages.
  • [Line] Fixed- The app will crash if unsupported sticker was sent.
  • [Google translation] Fixed- The translation result will be displayed incorrectly in some case.
  • [Google translation] Added- Now, you can translate into over 100 languages.(⁂Some languages(character) will be garbale.)

v1.4.0 2020/04/26

  • [Line] Added- send a file function.
  • [Image viewer] Added- view files on SD card.
  • [GAS] Fixed- The problem of losing messages from being sent too quickly have been resolved.

v1.3.1 2020/03/31

  • [Line] Fixed Common occurring [FSUSER_OpenArchive failed] error has been solved.
  • [Line] Fixed Image display now activated by touching the message.
  • [Setting] Fixed Setting menu framerate now improved.
  • [Image viewer] Fixed Image display speed now improved.

v1.3.0 2020/03/25

  • [Line] Added- sticker send and receive.

v1.2.2 2020/05/17

  • [Setting] Added- RAM allocation control menu.
  • [Line] Fixed- Now, some errors have been handled.
  • [Image viewer] Fixed- Now, some errors have been handled.
  • [Speedtest] Fixed- Now, some errors have been handled.

v1.2.1 2020/03/03

  • [App] Changed title id to 000400000EC95000.
  • [Setting] Added- Fonts setting.
  • [Setting] Added- Scroll bar.
  • [Line] Fixed- Auto update was destroyed.
  • [Line] Added- Hide ID.
  • [Line] Added- Scroll bar.
  • [Image viewer] Fixed- clipboard.
  • [Pull requests] Fixed- #3.

v1.2.0 2020/01/17

  • Fix Some crashes.
  • Fix Some settings do not work.
  • Fix Log download (GAS processing) speed has been improved(GAS update required).
  • Add Image viewer.

Release notes.

External links

Advertising: