Extension devepment tutorial - Mishiranu/Dashchan-Extensions GitHub Wiki

Contents

To be continued...

Foreword

For the first I want to warn you that it's not easy to make an extension from scratch. But it's not a problem: there are a lot of ready extensions in my repositories. All that you need is download them and change them a little.

Imageboard Software Extension Implementation Comment
Wakaba shanachan Pure Wakaba
nowere Almost pure Wakaba
cirno Extended Wakaba
Kusaba chuckdfwk Almost pure Kusaba or its fork
chiochan Extended Kusaba
nulleu Instant-0chan
Tinyboard / vichan haruhichan With API
lainchan With API
ponychan Without API, extended Tinyboard
infinite Infinity fork
FoolFuuka desustorage 4chan archive

Basics

Every chan extension must extend 4 classes: ChanConfiguration, ChanPerformer, ChanLocator and ChanMarkup.

ChanConfiguration used to determine what chan does: posting, deleting, loading captcha. It also used to store default names, board names and desctiptions.
ChanPerformer used to handle requests: reading threads, sending posts.
ChanLocator used to build URIs and determine the type of URIs.
ChanMarkup used to handle markup buttons and posts text appearance.

These classes will be dynamically loaded by client application. This is how every chan extension works.

You must include DashchanLibrary API library to your project but not compile. You can add this library from jcenter using Gradle:

dependencies {
    provided 'com.github.mishiranu:dashchan.library:+'
}

AndroidManifest.xml

There is an example of Android Manifest file:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.mishiranu.dashchan.chan.dvach"
	android:versionCode="1"
	android:versionName="1.8">
	
	<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
	
	<uses-feature android:name="chan.extension" />
	
	<application android:icon="@drawable/ic_launcher" android:allowBackup="false" android:label="@string/text_app_name">
		
		<meta-data android:name="chan.extension.name" android:value="dvach" />
		<meta-data android:name="chan.extension.version" android:value="1" />
		<meta-data android:name="chan.extension.icon" android:resource="@drawable/ic_custom_dvach_white" />
		<meta-data android:name="chan.extension.source" android:value="https://raw.githubusercontent.com/Mishiranu/Dashchan/master/update/data.json" />
		<meta-data android:name="chan.extension.class.configuration" android:value=".DvachChanConfiguration" />
		<meta-data android:name="chan.extension.class.performer" android:value=".DvachChanPerformer" />
		<meta-data android:name="chan.extension.class.locator" android:value=".DvachChanLocator" />
		<meta-data android:name="chan.extension.class.markup" android:value=".DvachChanMarkup" />
		
		<activity android:name="chan.app.UriHandlerActivity" android:label="@string/text_activity_name"
		    android:theme="@android:style/Theme.NoDisplay">
			
			<intent-filter>
				<action android:name="android.intent.action.VIEW" />
				<category android:name="android.intent.category.DEFAULT" />
				<category android:name="android.intent.category.BROWSABLE" />
				<data android:scheme="http" />
				<data android:scheme="https" />
				<data android:host="2ch.hk" />
				<data android:host="2ch.pm" />
				<data android:host="2ch.cm" />
				<data android:host="2ch.re" />
				<data android:host="2ch.tf" />
				<data android:host="2ch.wf" />
				<data android:host="2ch.yt" />
				<data android:host="2-ch.so" />
			</intent-filter>
			
		</activity>
		
	</application>
	
</manifest>

Every extension must use chan.extension feature.

In application block you need to declare the following meta datas:

Name Value description
chan.extension.name A short name of your extension (4-15 lowercase a-z chars)
chan.extension.version API version
chan.extension.icon 24dp x 24dp white icon identifier
chan.extension.source JSON file URI containing updating data
chan.extension.class.configuration ChanConfiguration implementation class name
chan.extension.class.performer ChanPerformer implementation class name
chan.extension.class.locator ChanLocator implementation class name
chan.extension.class.markup ChanMarkup implementation class name

Also your app may contain an activity. It's implemented in DashchanStatic library. You can add this library from jcenter using Gradle:

dependencies {
    compile 'com.github.mishiranu:dashchan.static:+'
}

You just need to declare an activity with chan.app.UriHandlerActivity name and add all possible hosts/schemes to intent-filter.

Chan Implementation

You can read a Javadoc that describes every Chan* class.

You also can use ready extensions as an example.

ChanConfiguration

Javadoc.

In constuctor you can request features and apply static configuration.

You also need to implement some configuration getters (obtain* methods) to configure the client.

ChanLocator

Javadoc.

The Javadoc describes what methods you must implements. Just do it.

ChanMarkup

Javadoc.

This class tells the client how to handle post markup. You can make text bold, italic, spoiled, etc.

If your extensions supports posting, you also must provide CommentEditor instance.

ChanPerformer

Javadoc.

This is the hardest part to implement. Every method of this class called by the client to perform requests to the server and handle response.

Read the Javadoc to understand what methods, when and how you must implement.

Examples

ChanLocator Implementation

Constructor

For example, the iichan.hk imageboard has 3 domains: iichan.hk, n.iichan.hk and on.iichan.hk. You can define them in constructor:

addChanHost("iichan.hk");
addChanHost("n.iichan.hk");
addChanHost("on.iichan.hk");

In this case user can choose a preferred domain name in applications preferences.

In addition this imageboard can be accessed from iichan.moe, closed.iichan.hk, but without posting, so you can tell client to transform these links to user chosen one. These links will be not available in application preferences:

addConvertableChanHost("iichan.moe");
addConvertableChanHost("closed.iichan.hk");

If your imageboard supports HTTPS, you can specify it using setHttpsMode(HttpsMode.CONFIGURABLE); in constructor.

Working With Links

Let's see what links there are on your imageboard. In case of iichan.hk:

  • 0 board page [1]: http://iichan.hk/b/
  • 0 board page [2]: http://iichan.hk/b/index.html
  • N board page: http://iichan.hk/b/2.html
  • Catalog page: http://iichan.hk/b/catalogue.html
  • Thread page: http://iichan.hk/b/res/100500.html
  • Post in thread [1]: http://iichan.hk/b/res/100500.html#100500
  • Post in thread [2]: http://iichan.hk/b/res/100500.html#i100500
  • Attachment: http://iichan.hk/b/src/1400000000000.png
Implementing isBoardUri, isThreadUri and isAttachmentUri methods

You can do it using regular expressions for path. There is a convenient method to do it: isPathMatches.

  • Pattern for board path: /\w+(/((index|catalogue|\d+)\.html)?)?
  • Pattern for thread path: /\w+/res/\d+\.html
  • Pattern for attachment path: /\w+/src/\d+\.\w+

So this is how you should use them:

private static final Pattern BOARD_PATH = Pattern.compile("/\\w+(?:/(?:(?:index|catalogue|\\d+)\\.html)?)?");
private static final Pattern THREAD_PATH = Pattern.compile("/\\w+/res/\\d+\\.html");
private static final Pattern ATTACHMENT_PATH = Pattern.compile("/\\w+/src/\\d+\\.\\w+");

@Override
public boolean isBoardUri(Uri uri)
{
    return isChanHostOrRelative(uri) && isPathMatches(uri, BOARD_PATH);
}

@Override
public boolean isThreadUri(Uri uri)
{
    return isChanHostOrRelative(uri) && isPathMatches(uri, THREAD_PATH);
}

@Override
public boolean isAttachmentUri(Uri uri)
{
    return isChanHostOrRelative(uri) && isPathMatches(uri, ATTACHMENT_PATH);
}
Implementing getBoardName, getThreadNumber and getPostNumber methods

These methods can be called for all types of URI: board, thread or attachment.

In case of board name you can just extract it from the first path segment:

@Override
public String getBoardName(Uri uri)
{
   List<String> segments = uri.getPathSegments();
   if (segments.size() > 0) return segments.get(0);
   return null;
}

The thread number is harder because it only present in thread URI. You can modify the regex above and extract thread number using it (added group parentheses around \d+ in the end of pattern). Also there is a convenient method getGroupValue for fast extracting group value.

private static final Pattern THREAD_PATH = Pattern.compile("/\\w+/(?:arch/)?res/(\\d+)\\.html");

@Override
public String getThreadNumber(Uri uri)
{
    return uri != null ? getGroupValue(uri.getPath(), THREAD_PATH, 1) : null;
}

The post number contains in thread URI fragment: #100500 or #i100500. In case of i I can just return a substring:

@Override
public String getPostNumber(Uri uri)
{
   String fragment = uri.getFragment();
   if (fragment != null && fragment.startsWith("i")) return fragment.substring(1);
   return fragment;
}
URI Building

ChanLocator also must build URI. It's not too hard, there are some convenient methods such as buildPath and buildQuery to do it:

@Override
public Uri createBoardUri(String boardName, int pageNumber)
{
    return pageNumber > 0 ? buildPath(boardName, pageNumber + ".html") : buildPath(boardName, "");
}

@Override
public Uri createThreadUri(String boardName, String threadNumber)
{
    return buildPath(boardName, "res", threadNumber + ".html");
}

@Override
public Uri createPostUri(String boardName, String threadNumber, String postNumber)
{
    return createThreadUri(boardName, threadNumber).buildUpon().fragment(postNumber).build();
}

ChanConfiguration Implementation

Constructor

For example, the 2ch.hk imageboard can read thread partially, read single post knowing its number and read posts count (which is important for thread watcher). Using request method you can request these options:

request(OPTION_READ_THREAD_PARTIALLY);
request(OPTION_READ_SINGLE_POST);
request(OPTION_READ_POSTS_COUNT);

You also can specify a default name and bump limit for all boards:

setDefaultName("Аноним");
setBumpLimit(500);

Dynamic Configuration

Sometimes client can request some information about your extension. For example, your extension supports reading catalog, posting and sending reports. This configuration is dynamic and can differ on different boards. You can provide it overriding the obtainBoardConfiguration method:

@Override
public Board obtainBoardConfiguration(String boardName)
{
    Board board = new Board();
    board.allowCatalog = true;
    board.allowPosting = true;
    board.allowReporting = true;
    return board;
}

In this case client can request from you more detailed information for posting and reporting using obtainPostingConfiguration and obtainReportingConfiguration methods. For example:

@Override
public Posting obtainPostingConfiguration(String boardName, boolean replying)
{
    Posting posting = new Posting();
    posting.allowName = true;
    posting.allowTripcode = true;
    posting.allowEmail = true;
    posting.allowSubject = true;
    posting.optionSage = true;
    posting.attachmentCount = 1;
    posting.attachmentMimeTypes.add("image/*");
    posting.attachmentMimeTypes.add("video/webm");
    return posting;
}

@Override
public Reporting obtainReportingConfiguration(String boardName)
{
    Reporting reporting = new Reporting();
    reporting.comment = true;
    reporting.multiplePosts = true;
    return reporting;
}

Sometimes your configuration may depend on information you retrieved from site. For example, some boards doesn't support names and emails. You can store this data in client storage. For the first, create constants:

private static final String KEY_NAMES_ENABLED = "names_enabled";
private static final String KEY_TRIPCODES_ENABLED = "tripcodes_enabled";
private static final String KEY_EMAILS_ENABLED = "emails_enabled";

Then modify obtainPostingConfiguration method using get method to access your storage data:

@Override
public Posting obtainPostingConfiguration(String boardName, boolean replying)
{
    Posting posting = new Posting();
    posting.allowName = get(boardName, KEY_NAMES_ENABLED, true);
    posting.allowTripcode = posting.allowName;
    posting.allowEmail = get(boardName, KEY_EMAILS_ENABLED, true);
    posting.allowSubject = true;
    posting.optionSage = true;
    posting.attachmentCount = 1;
    posting.attachmentMimeTypes.add("image/*");
    posting.attachmentMimeTypes.add("video/webm");
    return posting;
}

Using set method you can edit these parameters where you need:

boolean namesEnabled = ...;
boolean emailsEnabled = ...;
set(boardName, KEY_NAMES_ENABLED, namesEnabled);
set(boardName, KEY_EMAILS_ENABLED, emailsEnabled);

These methods work with boolean, int and String types.

ChanMarkup Implementation

Constructor

During construction you can tell HTML parser how to handle posts comment.

For example, <b>...</b> tags are used for bold text, <i>...</i> for italic, <span class="unkfunc">...</span> for quotes and <span class="spoiler">...</span> for spoilers:

addTag("b", TAG_BOLD);
addTag("i", TAG_ITALIC);
addTag("span", "unkfunc", TAG_QUOTE);
addTag("span", "spoiler", TAG_SPOILER);

CommentEditor

This class is used when user makes a post. For example, [b]...[/b] tag is used for bold text, [i]...[/i] tag is used for italic and '[spoiler]...[/spoiler]' is used for spoilers:

@Override
public CommentEditor obtainCommentEditor(String boardName)
{
    CommentEditor commentEditor = new CommentEditor();
    commentEditor.addTag(TAG_BOLD, "[b]", "[/b]");
    commentEditor.addTag(TAG_ITALIC, "[i]", "[/i]");
    commentEditor.addTag(TAG_SPOILER, "[spoiler]", "[/spoiler]");
    return commentEditor;
}

The quotes are always formatted with > in the beginning of the line.

There are some ready comment editors in API. For example, for BBCodes which is named BulletinBoardCodeCommentEditor. So you can simplify the method above:

@Override
public CommentEditor obtainCommentEditor(String boardName)
{
    return new CommentEditor.BulletinBoardCodeCommentEditor();
}

You also must specify what tags your client supported. There is a isTagSupported method for this purpose you should override. In case of bold, italic and spoilers:

private static final int SUPPORTED_TAGS = TAG_BOLD | TAG_ITALIC | TAG_SPOILER;

@Override
public boolean isTagSupported(String boardName, int tag)
{
    return (SUPPORTED_TAGS & tag) == tag;
}

You can use bit operations for TAG_* constants. The quotation tag is always supported.

Post Parsing Acceleration

This is not necessary, but highly desirable to implement obtainPostLinkThreadPostNumbers method. This method is used to extract thread number and post number from comment link. ChanLocator can do it by default, but you can make it faster just implementing this method:

private static final Pattern THREAD_LINK = Pattern.compile("(\\d+).html(?:#(\\d+))?$");

@Override
public Pair<String, String> obtainPostLinkThreadPostNumbers(String uriString)
{
    Matcher matcher = THREAD_LINK.matcher(uriString);
    if (matcher.find()) return new Pair<>(matcher.group(1), matcher.group(2));
    return null;
}

Working With Post Model

Every Post item has its own post number. You can specify it using setPostNumber method.

Every Post item which is not an original post has a "link" with original post. This link is named parent post number. You can specify it using setParentPostNumber method. You must specify it for all posts which are not original.

In most cases the number of original post is a thread number. But in some rare cases they are differ. You can specify thread number using setThreadNumber method.

So, if client requests the thread from your extension, you must provide an array of posts, where the first post is original and the rest posts are not. If client requests a single post, you can specify the posts original post number, so client can understand, whether your provided post is original.

⚠️ **GitHub.com Fallback** ⚠️